import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart';
import 'package:website.core/core/domain/clients/api_client.dart';
import 'package:website.core/core/domain/config/config.dart';
import 'package:website.core/core/domain/entities/access_credentials.dart';
import 'package:website.core/core/domain/entities/access_token.dart';
import 'package:website.core/core/domain/errors/exceptions.dart';
import 'package:website.core/core/infrastructure/dtos/api/api_access_credentials_dto.dart';

ApiClient createApiClient({
  required Client baseClient,
  AccessCredentials? accessCredentials,
}) {
  return _ApiClientImpl(
    accessCredentials: accessCredentials,
    baseClient: baseClient,
  );
}

class _ApiClientImpl extends BaseClient implements ApiClient {
  _ApiClientImpl({
    required Client baseClient,
    AccessCredentials? accessCredentials,
  })  : _baseClient = baseClient,
        _accessCredentials = accessCredentials;

  final Client _baseClient;

  final StreamController<AccessCredentials?> _credentialStreamController =
      StreamController.broadcast(sync: true);

  AccessCredentials? _accessCredentials;

  @override
  Stream<AccessCredentials?> get accessCredentialUpdates => _credentialStreamController.stream;

  @override
  void setAccessCredentials(AccessCredentials? accessCredentials) {
    if (_accessCredentials != accessCredentials) {
      _accessCredentials = accessCredentials;
      _notifyAboutNewCredentials(_accessCredentials);
    }
  }

  @override
  Future<StreamedResponse> send(BaseRequest request) async {
    final modifiedRequest = _RequestOverride(request);

    if (_accessCredentials?.accessToken.isExpired == true) {
      _accessCredentials = await _obtainNewCredentials(
        refreshToken: _accessCredentials!.refreshToken,
      );

      _notifyAboutNewCredentials(_accessCredentials);
    }

    if (_accessCredentials != null) {
      _setAuthorizationHeader(
        request: modifiedRequest,
        accessToken: _accessCredentials!.accessToken,
      );
    }

    final response = await _baseClient.send(modifiedRequest);

    if (response.statusCode == 401 && _accessCredentials != null) {
      _accessCredentials = await _obtainNewCredentials(
        refreshToken: _accessCredentials!.refreshToken,
      );

      _notifyAboutNewCredentials(_accessCredentials);

      if (_accessCredentials == null) {
        return response;
      }

      _setAuthorizationHeader(
        request: modifiedRequest,
        accessToken: _accessCredentials!.accessToken,
      );

      return _baseClient.send(modifiedRequest);
    }

    return response;
  }

  Future<AccessCredentials?> _obtainNewCredentials({
    required String refreshToken,
  }) async {
    final payload = {
      'refresh_token': refreshToken,
    };

    final requestBody = jsonEncode(payload);

    final url = Uri.parse('${Config().apiBaseUrl}/refresh-token');

    const headers = {
      'Content-Type': 'application/json',
    };

    final response = await _baseClient.post(
      url,
      body: requestBody,
      headers: headers,
    );

    if (response.statusCode == 401) {
      return null;
    }

    if (response.statusCode >= 500) {
      throw const ServerException();
    }

    if (response.statusCode != 200) {
      throw UnimplementedError();
    }

    final responseBody = jsonDecode(response.body) as Map<String, dynamic>;
    final accessCredentialsDto = ApiAccessCredentialsDto.fromJson(responseBody);
    final accessCredentials = accessCredentialsDto.toEntity();

    return accessCredentials;
  }

  void _setAuthorizationHeader({
    required BaseRequest request,
    required AccessToken accessToken,
  }) {
    request.headers['Authorization'] = 'Bearer ${accessToken.data}';
  }

  void _notifyAboutNewCredentials(AccessCredentials? accessCredentials) {
    _credentialStreamController.add(accessCredentials);
  }

  @override
  void close() {
    _credentialStreamController.close();
    super.close();
  }
}

class _RequestOverride extends BaseRequest {
  _RequestOverride(BaseRequest baseRequest) : super(baseRequest.method, baseRequest.url) {
    _baseRequestFinalizationStream = baseRequest.finalize();
    headers.addAll(baseRequest.headers);
  }

  late final ByteStream _baseRequestFinalizationStream;

  @override
  ByteStream finalize() {
    if (!finalized) {
      super.finalize();
    }

    return _baseRequestFinalizationStream;
  }
}
